[do not merge] feat: Span streaming & new span API#5317
[do not merge] feat: Span streaming & new span API#5317sentrivana wants to merge 129 commits intomasterfrom
Conversation
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Bug Fixes 🐛Openai
Other
Documentation 📚
Internal Changes 🔧
Other
🤖 This preview updates automatically when you update the PR. |
| transaction = sentry_sdk.traces.start_span( | ||
| name="unknown celery task" | ||
| ) | ||
| transaction.set_origin(CeleryIntegration.origin) | ||
| transaction.set_source(TransactionSource.TASK) | ||
| transaction.set_op(OP.QUEUE_TASK_CELERY) | ||
|
|
||
| span_ctx = transaction |
There was a problem hiding this comment.
Celery task name not set in span streaming mode
In the span streaming path, the transaction is created with name="unknown celery task" but never updated to the actual task name. The non-streaming path sets transaction.name = task.name (line 349), but the streaming path is missing the equivalent transaction.set_name(task.name) call. This will cause all Celery tasks in span streaming mode to be reported as "unknown celery task" in Sentry, making task identification and debugging impossible.
Verification
Read sentry_sdk/integrations/celery/init.py lines 303-370 to compare the span_streaming and non-streaming code paths. Verified StreamedSpan has a set_name() method (sentry_sdk/traces.py line 478-479) that should be called. Non-streaming path explicitly sets transaction.name = task.name on line 349, but span_streaming path lacks equivalent.
Suggested fix: Add set_name(task.name) call after creating the span in the span streaming path
| transaction = sentry_sdk.traces.start_span( | |
| name="unknown celery task" | |
| ) | |
| transaction.set_origin(CeleryIntegration.origin) | |
| transaction.set_source(TransactionSource.TASK) | |
| transaction.set_op(OP.QUEUE_TASK_CELERY) | |
| span_ctx = transaction | |
| transaction.set_name(task.name) |
Identified by Warden code-review · 8DB-RN4
| if isinstance(span, Span) and span.status == SPANSTATUS.INTERNAL_ERROR: | ||
| with capture_internal_exceptions(): | ||
| span.__exit__(None, None, None) |
There was a problem hiding this comment.
StreamedSpan not properly closed on error in Anthropic integration
The change filters out StreamedSpan instances from error cleanup, but doesn't add equivalent handling for the new span type. When streaming mode is enabled (trace_lifecycle: stream) and an exception occurs during Anthropic API calls, the StreamedSpan will have its status set to SpanStatus.ERROR by set_span_errored(), but __exit__ will never be called to properly end the span. This leaves the span unclosed and potentially causes incomplete trace data.
Verification
Read sentry_sdk/integrations/anthropic.py lines 540-614 to understand the span lifecycle. Verified that span is created via get_start_span_function() which can return StreamedSpan when streaming mode is enabled (sentry_sdk/ai/utils.py:535-547). Confirmed StreamedSpan uses get_status() method and SpanStatus.ERROR (not .status attribute with SPANSTATUS.INTERNAL_ERROR) per sentry_sdk/traces.py:455-465. Checked set_span_errored() in sentry_sdk/tracing_utils.py:1109-1126 confirms it sets SpanStatus.ERROR on StreamedSpan. The isinstance(span, Span) check excludes StreamedSpan from exit cleanup.
Also found at 1 additional location
sentry_sdk/integrations/celery/__init__.py:104-105
Identified by Warden code-review · SR8-YQM
| # to add @functools.wraps(f) here. | ||
| # https://github.com/getsentry/sentry-python/issues/421 | ||
| @ensure_integration_enabled(CeleryIntegration, f) | ||
| def _inner(*args: "Any", **kwargs: "Any") -> "Any": |
There was a problem hiding this comment.
Missing @wraps(f) decorator after removing @ensure_integration_enabled breaks celery-once compatibility
The @ensure_integration_enabled(CeleryIntegration, f) decorator was removed from _inner, but @functools.wraps(f) was not added. The comment at lines 395-399 explicitly warns: "functools.wraps is important here because celery-once looks at this method's name... but if we ever remove the @ensure_integration_enabled decorator, we need to add @functools.wraps(f) here." Without @wraps(f), _inner.__name__ will be '_inner' instead of the original function's name, breaking celery-once integration.
Verification
Read the _wrap_task_call function in sentry_sdk/integrations/celery/init.py (lines 391-464). Verified the ensure_integration_enabled decorator in sentry_sdk/utils.py (lines 1837-1849) applies wraps(original_function) at line 1847. The comment at lines 395-399 explicitly documents this requirement. The decorator was removed but @wraps(f) was not added to _inner.
Suggested fix: Add @wraps(f) decorator to the _inner function to preserve the original function's metadata
| def _inner(*args: "Any", **kwargs: "Any") -> "Any": | |
| @wraps(f) |
Identified by Warden code-review · 7EE-X7V
| name=sentry_span_name, parent_span=parent_sentry_span | ||
| ) | ||
| sentry_span.set_op("function") | ||
| sentry_span.set_origin("origin") |
There was a problem hiding this comment.
set_origin uses literal string "origin" instead of self.origin
On line 220, sentry_span.set_origin("origin") uses the hardcoded string literal "origin" instead of self.origin. This is inconsistent with all other branches in the same function that correctly use self.origin (lines 225, 235, 240). This causes spans created under a StreamedSpan parent to have an incorrect origin attribute ("origin" instead of the properly configured value like "auto.function.rust_tracing.{identifier}").
Verification
Read the full file sentry_sdk/integrations/rust_tracing.py. Confirmed self.origin is initialized in RustTracingLayer.init (line 159) and is used correctly in all other code paths: line 225 (start_child), line 235 (set_origin with no parent in streaming mode), line 240 (start_span with no parent in non-streaming mode). Only line 220 incorrectly uses the string literal.
Suggested fix: Change the string literal "origin" to self.origin
| sentry_span.set_origin("origin") | |
| sentry_span.set_origin(self.origin) |
Identified by Warden code-review · 8Y6-99J
| span.set_attribute(SPANDATA.NETWORK_PEER_ADDRESS, self.host) | ||
| span.set_attribute(SPANDATA.NETWORK_PEER_PORT, self.port) | ||
|
|
||
| span.start() |
There was a problem hiding this comment.
NoOpStreamedSpan.finish() does not restore scope state
When span_streaming is enabled and a NoOpStreamedSpan is returned (e.g., for ignored spans), calling span.start() followed by span.finish() does not properly restore the scope state. NoOpStreamedSpan.start() sets scope.span = self and saves the old span in _context_manager_state, but NoOpStreamedSpan.finish() is a no-op (pass), so scope.span is never restored. This could cause subsequent spans to be incorrectly parented to the no-op span.
Verification
Verified by reading sentry_sdk/traces.py: NoOpStreamedSpan.finish() (line 727-728) is pass, while NoOpStreamedSpan.start() (line 721-722) calls enter() which sets scope.span. The getresponse() function at line 183 calls span.finish() which won't clean up for NoOpStreamedSpan.
Suggested fix: Call span.end() instead of span.finish() since NoOpStreamedSpan.end() properly calls exit() to restore scope state, or fix NoOpStreamedSpan.finish() to also call exit()
| span.start() | |
| if isinstance(span, StreamedSpan): | |
| span.end() | |
| else: | |
| span.finish() |
Identified by Warden code-review · FYA-VCQ
| self.parsing_span = sentry_sdk.traces.start_span( | ||
| name="parsing", | ||
| ) |
There was a problem hiding this comment.
Missing parent_span in on_parse() causes orphaned spans in streaming mode
In on_parse(), the start_span() call for streaming mode (lines 261-263) does not pass parent_span=self.graphql_span, unlike the equivalent code in on_validate() which correctly passes it. This means parsing spans will not be correctly parented to the GraphQL operation span in streaming mode, causing broken trace hierarchy and incorrect trace visualization in Sentry.
Verification
Compared on_parse() streaming path (line 261-263) with on_validate() streaming path (line 239-241) in strawberry.py. Also verified against resolve() methods in both SentryAsyncExtension (line 314-315) and SentrySyncExtension (line 352-353) which all correctly pass parent_span. Confirmed start_span API signature in sentry_sdk/traces.py (line 105-109) which shows parent_span is optional and defaults to None (current active span).
Suggested fix: Add parent_span=self.graphql_span to the start_span() call in on_parse() for streaming mode, matching the pattern used in on_validate().
| self.parsing_span = sentry_sdk.traces.start_span( | |
| name="parsing", | |
| ) | |
| parent_span=self.graphql_span, |
Identified by Warden code-review · 9Y5-4C6
| if isinstance(self.graphql_span, StreamedSpan): | ||
| span = sentry_sdk.traces.start_span( | ||
| parent_span=self.graphql_span, | ||
| name="resolving {field_path}", |
There was a problem hiding this comment.
Missing f-string prefix causes span name to be literal text instead of formatted
In SentrySyncExtension.resolve, the span name is set to "resolving {field_path}" (line 354) but the f prefix is missing. This means the span name will literally be "resolving {field_path}" instead of being interpolated with the actual field path value (e.g., "resolving Query.users"). The async version on line 315 correctly uses an f-string. This will make it difficult to identify which resolver a span corresponds to in Sentry.
Verification
Read the full file and compared line 354 (name="resolving {field_path}") with line 315 (name=f"resolving {field_path}"). The async version has the f prefix while the sync version is missing it.
Suggested fix: Add the f prefix to make it an f-string
| name="resolving {field_path}", | |
| name=f"resolving {field_path}", |
Identified by Warden code-review · D84-YUV
| if self.sampled is None: | ||
| logger.warning("Discarding transaction without sampling decision.") |
There was a problem hiding this comment.
Span not discarded despite 'discarding' warning when sampled is None
At line 418-419, when self.sampled is None, the code logs 'Discarding transaction without sampling decision' but does not return. This allows the span to continue processing and potentially be captured at line 434-435. The legacy implementation in tracing.py (line 1036-1038) correctly returns None after this warning. This inconsistency means spans without sampling decisions may be sent when they should be discarded.
Verification
Compared to sentry_sdk/tracing.py lines 1036-1038 which has return None after the same warning. In traces.py, after line 419 the code continues to line 421 and may reach line 434-435 where the span is captured.
Suggested fix: Add a return statement after the warning to actually discard the span, matching the legacy behavior in tracing.py
| if self.sampled is None: | |
| logger.warning("Discarding transaction without sampling decision.") | |
| return |
Identified by Warden code-review · DBB-FK8
| class NoOpStreamedSpan(StreamedSpan): | ||
| __slots__ = ( | ||
| "_name", | ||
| "segment", | ||
| "_scope", | ||
| "_context_manager_state", | ||
| ) |
There was a problem hiding this comment.
NoOpStreamedSpan missing method overrides will cause AttributeError
NoOpStreamedSpan inherits from StreamedSpan but does not override dynamic_sampling_context(), get_baggage(), or to_baggage(). Since NoOpStreamedSpan sets self.segment = None and does not define _baggage in its __slots__, calling these inherited methods will raise AttributeError. For example, dynamic_sampling_context() at line 524-525 calls self.segment.get_baggage() which will fail with 'NoneType' object has no attribute 'get_baggage'.
Verification
Verified by reading NoOpStreamedSpan class definition (lines 670-776) and its parent StreamedSpan. NoOpStreamedSpan's slots (lines 671-676) does not include '_baggage', and init sets segment=None. The methods dynamic_sampling_context() (line 524-525), get_baggage() (lines 573-582) are not overridden.
Identified by Warden code-review · RKZ-FNB
| assert segment2["is_segment"] is True | ||
| assert segment2["parent_span_id"] is None | ||
|
|
||
| assert segment1["trace_id"] == segment1["trace_id"] |
There was a problem hiding this comment.
Tautological assertion compares segment1 trace_id to itself
Line 500 asserts segment1["trace_id"] == segment1["trace_id"] which always passes. Based on the test name test_sibling_segments and the pattern in the subsequent test test_sibling_segments_new_trace (which asserts segment1["trace_id"] != segment2["trace_id"]), this should verify that sibling segments without a new_trace() call share the same trace_id: assert segment1["trace_id"] == segment2["trace_id"]. The test currently provides no coverage for this expected behavior.
Verification
Read lines 470-535 of test_span_streaming.py. Compared test_sibling_segments (line 470) with test_sibling_segments_new_trace (line 503). The latter correctly asserts segment1["trace_id"] != segment2["trace_id"] on line 535, confirming the pattern. The test name and structure indicate line 500 should assert both segments share the same trace_id.
Suggested fix: Compare segment1's trace_id with segment2's trace_id instead of itself
| assert segment1["trace_id"] == segment1["trace_id"] | |
| assert segment1["trace_id"] == segment2["trace_id"] |
Identified by Warden code-review · PVK-5ZM
| if isinstance(current_span, StreamedSpan) or has_span_streaming_enabled( | ||
| client.options | ||
| ): | ||
| return sentry_sdk.traces.start_span |
There was a problem hiding this comment.
get_start_span_function returns incompatible function signature in streaming mode
When span streaming is enabled, get_start_span_function() returns sentry_sdk.traces.start_span which only accepts name, attributes, and parent_span parameters. However, callers (google_genai, anthropic, langchain, pydantic_ai, mcp, litellm integrations) pass op, name, and origin as keyword arguments. This will cause a TypeError at runtime when users enable streaming mode with _experiments={"trace_lifecycle": "stream"} and any of these AI integrations are used.
Verification
Verified by reading sentry_sdk/traces.py lines 105-156 showing start_span signature accepts only (name, attributes, parent_span). Traced callers via grep for get_start_span_function() showing integrations pass op= and origin= kwargs (e.g., google_genai/init.py:76-80, anthropic.py:408-412, langchain.py:982-985). The signature mismatch will raise TypeError when streaming mode is enabled.
Suggested fix: Update sentry_sdk.traces.start_span to accept and handle **kwargs, or update the function to accept op and origin parameters and forward them appropriately
| return sentry_sdk.traces.start_span | |
| op: "Optional[str]" = None, | |
| origin: "Optional[str]" = None, | |
| span = sentry_sdk.get_current_scope().start_streamed_span( | |
| if op is not None: | |
| span.set_op(op) | |
| if origin is not None: | |
| span.set_origin(origin) | |
| return span |
Also found at 2 additional locations
sentry_sdk/integrations/anthropic.py:610-612sentry_sdk/integrations/celery/__init__.py:104-105
Identified by Warden find-bugs · UV8-FTU
| _graphql_span = sentry_sdk.traces.start_span(name=operation_name or "operation") | ||
| _graphql_span.set_op(op) | ||
| _graphql_span.set_attribute("graphql.document", source) | ||
| if operation_name: | ||
| _graphql_span.set_attribute("graphql.operation.name", operation_name) | ||
| _graphql_span.set_attribute("graphql.operation.type", operation_type) |
There was a problem hiding this comment.
StreamedSpan created without entering context manager causes silent span loss
In streaming mode, the code creates a StreamedSpan via sentry_sdk.traces.start_span() but never enters its context manager (no .start() call or with statement). When .finish() is called in the finally block, it invokes __exit__() which tries to access self._context_manager_state - an attribute only set in __enter__(). Since this access is wrapped in capture_internal_exceptions(), the AttributeError is silently caught and logged, but _end() is never called. This means GraphQL spans in streaming mode are never actually sent to Sentry.
Verification
Verified by tracing the code path: 1) sentry_sdk.traces.start_span() returns a StreamedSpan without calling __enter__. 2) StreamedSpan.__init__ does not set _context_manager_state. 3) _graphql_span.finish() (line 166) calls end() which calls __exit__(). 4) __exit__ (traces.py:346-350) accesses self._context_manager_state inside capture_internal_exceptions(), which swallows the AttributeError. 5) _end() is never reached because the exception is caught. Confirmed other integrations (e.g., celery/init.py:294) correctly use with span_mgr as span: pattern.
Suggested fix: Use the span as a context manager by calling .start() before the try/yield block and using .end() in the finally block, matching the pattern used in other integrations like Celery.
| _graphql_span = sentry_sdk.traces.start_span(name=operation_name or "operation") | |
| _graphql_span.set_op(op) | |
| _graphql_span.set_attribute("graphql.document", source) | |
| if operation_name: | |
| _graphql_span.set_attribute("graphql.operation.name", operation_name) | |
| _graphql_span.set_attribute("graphql.operation.type", operation_type) | |
| _graphql_span.start() | |
| _graphql_span.end() |
Also found at 1 additional location
sentry_sdk/integrations/stdlib.py:183-183
Identified by Warden find-bugs · H3C-HLQ
| span_ctx: "Union[Span, StreamedSpan]" | ||
|
|
||
| # Celery task objects are not a thing to be trusted. Even | ||
| # something such as attribute access can fail. | ||
| with capture_internal_exceptions(): | ||
| headers = args[3].get("headers") or {} | ||
| transaction = continue_trace( | ||
| headers, | ||
| op=OP.QUEUE_TASK_CELERY, | ||
| name="unknown celery task", | ||
| source=TransactionSource.TASK, | ||
| origin=CeleryIntegration.origin, | ||
| ) | ||
| transaction.name = task.name | ||
| transaction.set_status(SPANSTATUS.OK) | ||
| if span_streaming: | ||
| sentry_sdk.traces.continue_trace(headers) | ||
| transaction = sentry_sdk.traces.start_span( | ||
| name="unknown celery task" | ||
| ) | ||
| transaction.set_origin(CeleryIntegration.origin) | ||
| transaction.set_source(TransactionSource.TASK) | ||
| transaction.set_op(OP.QUEUE_TASK_CELERY) | ||
|
|
||
| span_ctx = transaction | ||
|
|
||
| else: | ||
| transaction = continue_trace( | ||
| headers, | ||
| op=OP.QUEUE_TASK_CELERY, | ||
| name="unknown celery task", | ||
| source=TransactionSource.TASK, | ||
| origin=CeleryIntegration.origin, | ||
| ) | ||
| transaction.name = task.name | ||
| transaction.set_status(SPANSTATUS.OK) | ||
|
|
||
| span_ctx = sentry_sdk.start_transaction( | ||
| transaction, | ||
| custom_sampling_context={ | ||
| "celery_job": { | ||
| "task": task.name, | ||
| # for some reason, args[1] is a list if non-empty but a | ||
| # tuple if empty | ||
| "args": list(args[1]), | ||
| "kwargs": args[2], | ||
| } | ||
| }, | ||
| ) | ||
|
|
||
| if transaction is None: | ||
| return f(*args, **kwargs) | ||
|
|
||
| with sentry_sdk.start_transaction( | ||
| transaction, | ||
| custom_sampling_context={ | ||
| "celery_job": { | ||
| "task": task.name, | ||
| # for some reason, args[1] is a list if non-empty but a | ||
| # tuple if empty | ||
| "args": list(args[1]), | ||
| "kwargs": args[2], | ||
| } | ||
| }, | ||
| ): | ||
| with span_ctx: |
There was a problem hiding this comment.
Potential UnboundLocalError for span_ctx variable if exception occurs
The variable span_ctx is declared but not initialized. Inside capture_internal_exceptions(), if an exception occurs after transaction = start_span(...) but before span_ctx = transaction (e.g., during set_origin, set_source, or set_op calls), the transaction is None check passes but span_ctx remains unbound. This would cause an UnboundLocalError at with span_ctx: on line 368.
Verification
Traced the control flow: span_ctx declared on line 324 without initialization. In span_streaming path, span_ctx = transaction is assigned on line 339. If exception occurs at lines 335-337 (set_origin/set_source/set_op), transaction is set but span_ctx is not. The check at line 365 only checks transaction is None, so execution proceeds to line 368 where span_ctx is used uninitialized.
Suggested fix: Initialize span_ctx to None and check both transaction and span_ctx before using
| span_ctx: "Union[Span, StreamedSpan]" | |
| # Celery task objects are not a thing to be trusted. Even | |
| # something such as attribute access can fail. | |
| with capture_internal_exceptions(): | |
| headers = args[3].get("headers") or {} | |
| transaction = continue_trace( | |
| headers, | |
| op=OP.QUEUE_TASK_CELERY, | |
| name="unknown celery task", | |
| source=TransactionSource.TASK, | |
| origin=CeleryIntegration.origin, | |
| ) | |
| transaction.name = task.name | |
| transaction.set_status(SPANSTATUS.OK) | |
| if span_streaming: | |
| sentry_sdk.traces.continue_trace(headers) | |
| transaction = sentry_sdk.traces.start_span( | |
| name="unknown celery task" | |
| ) | |
| transaction.set_origin(CeleryIntegration.origin) | |
| transaction.set_source(TransactionSource.TASK) | |
| transaction.set_op(OP.QUEUE_TASK_CELERY) | |
| span_ctx = transaction | |
| else: | |
| transaction = continue_trace( | |
| headers, | |
| op=OP.QUEUE_TASK_CELERY, | |
| name="unknown celery task", | |
| source=TransactionSource.TASK, | |
| origin=CeleryIntegration.origin, | |
| ) | |
| transaction.name = task.name | |
| transaction.set_status(SPANSTATUS.OK) | |
| span_ctx = sentry_sdk.start_transaction( | |
| transaction, | |
| custom_sampling_context={ | |
| "celery_job": { | |
| "task": task.name, | |
| # for some reason, args[1] is a list if non-empty but a | |
| # tuple if empty | |
| "args": list(args[1]), | |
| "kwargs": args[2], | |
| } | |
| }, | |
| ) | |
| if transaction is None: | |
| return f(*args, **kwargs) | |
| with sentry_sdk.start_transaction( | |
| transaction, | |
| custom_sampling_context={ | |
| "celery_job": { | |
| "task": task.name, | |
| # for some reason, args[1] is a list if non-empty but a | |
| # tuple if empty | |
| "args": list(args[1]), | |
| "kwargs": args[2], | |
| } | |
| }, | |
| ): | |
| with span_ctx: | |
| span_ctx: "Optional[Union[Span, StreamedSpan]]" = None | |
| if transaction is None or span_ctx is None: |
Identified by Warden find-bugs · SWU-CK2
| if isinstance(self._span, StreamedSpan): | ||
| self._span.segment.set_name(name) | ||
| if source: | ||
| self._span.segment.set_source(source) |
There was a problem hiding this comment.
set_transaction_name crashes when scope has NoOpStreamedSpan due to None segment
When self._span is a NoOpStreamedSpan, isinstance(self._span, StreamedSpan) returns True (since NoOpStreamedSpan inherits from StreamedSpan), but NoOpStreamedSpan.segment is set to None in its __init__. This causes self._span.segment.set_name(name) to raise AttributeError: 'NoneType' object has no attribute 'set_name'. Users can trigger this by calling scope.set_transaction_name() while a NoOpStreamedSpan is the active span (returned when spans are ignored or the SDK is misconfigured).
Verification
Traced code path: 1) NoOpStreamedSpan.init sets self.segment = None (traces.py:681), 2) NoOpStreamedSpan.enter sets scope.span = self (traces.py:693), 3) set_transaction_name checks isinstance(self._span, StreamedSpan) which is True for NoOpStreamedSpan, 4) Accessing self._span.segment.set_name() fails because segment is None.
Suggested fix: Add a check for NoOpStreamedSpan before accessing segment, or check if segment is not None
| if isinstance(self._span, StreamedSpan): | |
| self._span.segment.set_name(name) | |
| if source: | |
| self._span.segment.set_source(source) | |
| if isinstance(self._span, StreamedSpan) and not isinstance(self._span, NoOpStreamedSpan): |
Also found at 1 additional location
sentry_sdk/scope.py:1201-1203
Identified by Warden find-bugs · 2PK-MMZ
|
|
||
| new_trace() doesn't start any spans on its own. | ||
| """ | ||
| sentry_sdk.get_current_scope().set_new_propagation_context() |
There was a problem hiding this comment.
new_trace() doesn't reset propagation context on isolation scope, unlike continue_trace()
The continue_trace() function explicitly sets propagation context on both the isolation scope and current scope (with a comment explaining this is needed 'for compatibility reasons' in span-first mode). However, new_trace() only calls set_new_propagation_context() on the current scope. This asymmetry could lead to stale trace information persisting on the isolation scope when starting a new trace, potentially causing spans to be incorrectly associated with old traces in certain scope configurations.
Verification
Verified by reading both functions in sentry_sdk/traces.py (lines 159-191) and the comment in continue_trace() at lines 169-173 which explicitly states both scopes need to be set. Also verified that set_new_propagation_context() exists and is used on isolation scope in other parts of the codebase (e.g., sentry_sdk/integrations/celery/beat.py line 192).
Suggested fix: Call set_new_propagation_context() on both isolation scope and current scope for consistency with continue_trace()
| sentry_sdk.get_current_scope().set_new_propagation_context() | |
| sentry_sdk.get_isolation_scope().set_new_propagation_context() |
Identified by Warden find-bugs · 3EK-9CE
| elif isinstance(rule, dict): | ||
| name_matches = True | ||
| attributes_match = True | ||
|
|
||
| if "name" in rule: | ||
| name_matches = _matches(rule["name"], name) | ||
|
|
||
| if "attributes" in rule: | ||
| if not attributes: | ||
| attributes_match = False | ||
| else: | ||
| for attribute, value in rule["attributes"].items(): | ||
| if attribute not in attributes or not _matches( | ||
| value, attributes[attribute] | ||
| ): | ||
| attributes_match = False | ||
| break | ||
|
|
||
| if name_matches and attributes_match: | ||
| return True |
There was a problem hiding this comment.
Empty dict in ignore_spans config ignores all spans unexpectedly
When an empty dict {} is included in the _experiments.ignore_spans config, the is_ignored_span function will return True for all spans. This happens because name_matches and attributes_match both default to True, and without any 'name' or 'attributes' keys to check, the condition name_matches and attributes_match is always satisfied. This is likely a configuration error by users, and the behavior could cause unexpected loss of all tracing data.
Verification
Verified by reading sentry_sdk/tracing_utils.py lines 1498-1517. The dict rule handling initializes name_matches=True and attributes_match=True (lines 1499-1500), then only overwrites them if 'name' or 'attributes' keys exist (lines 1502-1514). If neither key exists, both remain True and the function returns True at line 1517. Checked tests/tracing/test_span_streaming.py and found no test case for empty dict rules.
Suggested fix: Add a check to skip dict rules that have neither 'name' nor 'attributes' keys, treating them as no-ops rather than match-all rules.
| elif isinstance(rule, dict): | |
| name_matches = True | |
| attributes_match = True | |
| if "name" in rule: | |
| name_matches = _matches(rule["name"], name) | |
| if "attributes" in rule: | |
| if not attributes: | |
| attributes_match = False | |
| else: | |
| for attribute, value in rule["attributes"].items(): | |
| if attribute not in attributes or not _matches( | |
| value, attributes[attribute] | |
| ): | |
| attributes_match = False | |
| break | |
| if name_matches and attributes_match: | |
| return True | |
| # Skip empty dict rules - they should not match anything | |
| if "name" not in rule and "attributes" not in rule: | |
| continue | |
Identified by Warden find-bugs · AY4-S9N
Introduce a new
start_span()API with a simpler and more intuitive signature to eventually replace the originalstart_span()andstart_transaction()APIs.Additionally, introduce a new streaming mode (
sentry_sdk.init(_experiments={"trace_lifecycle": "stream"})) that will send spans as they finish, rather than by transaction.The new API MUST be used with the new streaming mode, and the old API MUST be used in the legacy non-streaming (static) mode.
Migration guide: getsentry/sentry-docs#16072
Notes
Spanand drop the newStreamedSpanintracing.pyas a replacement.trace_id(we can't send spans from different traces in the same envelope).Release Plan
Project
https://linear.app/getsentry/project/span-first-sdk-python-727da28dd037/overview